﻿namespace Snipps.Infrastructure
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Diagnostics;
    using System.Windows;

    // Author: Matt Heffron
    // src: http://mvvmfoundation.codeplex.com/workitem/5453

    /// <summary>
    /// Monitors the CollectionChanged event of an object that implements INotifyCollectionChanged,
    ///     and executes callback methods (i.e. handlers) registered for actions of that object.
    /// </summary>
    /// <typeparam name="TCollectionSource">
    /// The type of object to monitor for collection changes.
    /// </typeparam>
    public class CollectionObserver<TCollectionSource> : IWeakEventListener
        where TCollectionSource : INotifyCollectionChanged
    {
        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="CollectionObserver{TCollectionSource}"/> class.
        ///     Initializes a new instance of CollectionObserver, which
        ///     observes the 'collectionSource' object for property changes.
        /// </summary>
        /// <param name="collectionSource">
        /// The object to monitor for property changes.
        /// </param>
        public CollectionObserver(TCollectionSource collectionSource)
        {
            if (collectionSource == null)
            {
                throw new ArgumentNullException("collectionSource");
            }

            _collectionSourceRef = new WeakReference(collectionSource);
            _actionToHandlerMap =
                new Dictionary<NotifyCollectionChangedAction, Action<TCollectionSource, NotifyCollectionChangedEventArgs>>();
        }

        #endregion // Constructor

        #region Public Methods

        #region RegisterHandler

        /// <summary>
        /// Registers a callback to be invoked when the CollectionChanged event has been raised for the specified property.
        /// </summary>
        /// <param name="changeAction">
        /// The kind of change action to register for.
        /// </param>
        /// <param name="handler">
        /// The callback to invoke when the action has changed the collection.
        /// </param>
        /// <returns>
        /// The object on which this method was invoked, to allow for multiple invocations chained together.
        /// </returns>
        public CollectionObserver<TCollectionSource> RegisterHandler(
            NotifyCollectionChangedAction changeAction, Action<TCollectionSource, NotifyCollectionChangedEventArgs> handler)
        {
            if (!Enum.IsDefined(typeof(NotifyCollectionChangedAction), changeAction))
            {
                throw new ArgumentException(
                    string.Format(
                        "ChangeAction ({0}) is not a recognized value of type NotifyCollectionChangedAction.", changeAction));
            }

            if (handler == null)
            {
                throw new ArgumentNullException("handler");
            }

            var propertySource = GetPropertySource();
            if (propertySource != null)
            {
                Debug.Assert(
                    !_actionToHandlerMap.ContainsKey(changeAction), 
                    "Why is the '" + changeAction + "' property being registered again?");

                _actionToHandlerMap[changeAction] = handler;
                CollectionChangedEventManager.AddListener(propertySource, this);
            }

            return this;
        }

        #endregion // RegisterHandler

        #region UnregisterHandler

        /// <summary>
        /// Removes the callback associated with the specified property.
        /// </summary>
        /// <param name="changeAction">
        /// The change Action.
        /// </param>
        /// <returns>
        /// The object on which this method was invoked, to allow for multiple invocations chained together.
        /// </returns>
        public CollectionObserver<TCollectionSource> UnregisterHandler(NotifyCollectionChangedAction changeAction)
        {
            if (!Enum.IsDefined(typeof(NotifyCollectionChangedAction), changeAction))
            {
                throw new ArgumentException(
                    string.Format(
                        "ChangeAction ({0}) is not a recognized value of type NotifyCollectionChangedAction.", changeAction));
            }

            var propertySource = GetPropertySource();
            if (propertySource != null)
            {
                if (_actionToHandlerMap.ContainsKey(changeAction))
                {
                    _actionToHandlerMap.Remove(changeAction);
                    CollectionChangedEventManager.RemoveListener(propertySource, this);
                }
            }

            return this;
        }

        #endregion // UnregisterHandler

        #endregion // Public Methods

        #region IWeakEventListener Members

        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            var handled = false;

            if (managerType == typeof(CollectionChangedEventManager))
            {
                var args = e as NotifyCollectionChangedEventArgs;
                if (args != null && sender is TCollectionSource)
                {
                    var propertySource = (TCollectionSource)sender;
                    var changeAction = args.Action;

                    Action<TCollectionSource, NotifyCollectionChangedEventArgs> handler;
                    if (_actionToHandlerMap.TryGetValue(changeAction, out handler))
                    {
                        handler(propertySource, args);

                        handled = true;
                    }
                }
            }

            // HACK: *must* return true or else this crashes with ExecutionEngineException!!!
            handled = true;
            return handled;
        }

        #endregion // IWeakEventListener Members

        #region Private Helpers

        #region GetPropertySource

        private TCollectionSource GetPropertySource()
        {
            try
            {
                return (TCollectionSource)_collectionSourceRef.Target;
            }
            catch
            {
                return default(TCollectionSource);
            }
        }

        #endregion // GetPropertySource

        #endregion // Private Helpers

        #region Fields

        private readonly Dictionary<NotifyCollectionChangedAction, Action<TCollectionSource, NotifyCollectionChangedEventArgs>> _actionToHandlerMap;

        private readonly WeakReference _collectionSourceRef;

        #endregion // Fields
    }
}